<?php

namespace PhpYes\Com;


use WgxConfig\RedisConfig;

final class UserAuth
{
    const TokenExpireTimeInterval = 1800; // 30 minutes

    private static $updateTokenTimeInterval = 600; // 10 minutes

    private static $signKey = '0F21oSCrhqVqSwhR8ElMeTniSzd1KhKRNLGQyZvSOzw=';
    private static $cipherName = 'AES-128-CBC';
    private static $cipherKey = 'uXrw31sZ4ALb9yN5';
    private static $iv = 'NE5Dm2IjBkZTCT2c';

    private static $singleObj = null;

    /**
     * @return UserAuth
     */
    public static function getSingleObj()
    {
        if (!self::$singleObj) {
            self::$singleObj = new UserAuth();
            self::$singleObj->extractTok();
        }

        return self::$singleObj;
    }

    private function __construct()
    {
    }

    // head
    private $exp;
    private $un;
    private $ln;

    // function
    private $tok;
    private $otok;
    private $needSync;

    /**
     * @throws UserAuthException
     */
    private function extractTok()
    {
        if (empty($_COOKIE['jwt'])) {
            return;
        }
        $jwt = trim($_COOKIE['jwt']);
        $arr = explode('.', $jwt);
        if (count($arr) != 3) {
            $this->needSync = true;
            throw new UserAuthException('jwt格式错误');
        }

        $head = $arr[0];
        $tok = $arr[1];
        $mySign = md5($head . '.' . $tok . self::$signKey);
        if ($mySign != $arr[2]) {
            $this->needSync = true;
            throw new UserAuthException('jwt签名错误');
        }

        // $headText = ''; // 测试使用
        $jsonText = Base64UrlSafeEncoder::decode($head);
        if (!$jsonText) {
            $this->needSync = true;
            throw new UserAuthException('jwt头错误');
        }
        $head = json_decode($jsonText, true);
        if (!$head) {
            $this->needSync = true;
            throw new UserAuthException('jwt头错误');
        }

        if (empty($head['exp'])) {
            $this->needSync = true;
            throw new UserAuthException('jwt头错误');
        }

        if (empty($head['un'])) {
            $this->needSync = true;
            throw new UserAuthException('jwt头错误');
        }
        if (empty($head['ln'])) {
            $this->needSync = true;
            throw new UserAuthException('jwt头错误');
        }

        $exp = intval($head['exp']);
        $nowTs = DateTimeHelper::timeNow();
        if ($exp <= $nowTs) {
            $this->needSync = true;
            throw new UserAuthException('jwt过期,请重新登录');
        }

        $this->exp = $exp;
        $this->un = trim($head['un']);
        $this->ln = intval($head['ln']);
        $this->tok = $tok;

        if (!empty($head['otok'])) {
            $this->needSync = true;
        }
        if ($exp - $nowTs < self::$updateTokenTimeInterval) {
            $this->genTok(false);
        }
    }

    /**
     * @param string $un
     * @param int|string $ln
     * @return string
     */
    private function buildTKey($un, $ln)
    {
        return 'tok-' . $un . '-' . $ln;
    }

    /**
     * @return null
     * @throws UserAuthException
     * @throws \Exception
     */
    private function getValidTok()
    {
        static $tokHaveBeenValidated = false;

        if ($tokHaveBeenValidated) {
            return $this->tok;
        }
        if (!$this->tok) {
            $tokHaveBeenValidated = true;

            return null;
        }

        $redis = WgxRedis::getRedisObj(RedisConfig::RedisDb);
        $tKey = $this->buildTKey($this->un, $this->ln);
        $tokStr = $redis->get($tKey);
        if (!$tokStr) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('token可能泄漏,请重新登录');
        }
        $tokArr = explode('.', $tokStr);
        if (!in_array($this->tok, $tokArr)) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('token可能泄漏,请重新登录');
        }

        $tokHaveBeenValidated = true;

        return $this->tok;
    }

    /**
     * @return int|null
     * @throws UserAuthException
     */
    public function getValidUid()
    {
        static $validUid = null;
        static $uidHaveBeenValidated = false;

        if ($uidHaveBeenValidated) {
            return $validUid;
        }
        $tok = $this->getValidTok();
        if (!$tok) {
            $uidHaveBeenValidated = true;

            return null;
        }

        $cipherText = Base64UrlSafeEncoder::decode($tok);
        if (!$cipherText) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('tok内容错误');
        }
        $jsonText = openssl_decrypt($cipherText, self::$cipherName, self::$cipherKey, true, self::$iv);
        if (!$jsonText) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('tok内容错误');
        }
        $tokInfo = json_decode($jsonText, true);
        if (!$tokInfo) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('tok内容错误');
        }

        if (empty($tokInfo['exp'])) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('tok内容错误');
        }
        if (empty($tokInfo['uid'])) {
            $this->tok = null;
            $this->needSync = true;
            throw new UserAuthException('tok内容错误');
        }

        $validUid = intval($tokInfo['uid']);
        $uidHaveBeenValidated = true;

        return $validUid;
    }

    /**
     * @param bool $loginOrUpdate - true: login, false: update
     * @param int $uid - $loginOrUpdate = true 时需要, $loginOrUpdate = false 时取 getValidUid
     * @return string
     * @throws UserAuthException
     */
    public function genTok($loginOrUpdate, $uid = 0)
    {
        $this->exp = DateTimeHelper::timeNow() + self::TokenExpireTimeInterval;

        if ($loginOrUpdate) { // create token
            $this->un = md5($uid . 'thsMGfGYTQ1Qeh1ohDRYItHKKjXpyw/dLuljcFKdaEY');
            $this->ln = $this->exp - 1488340800; // 1488340800 = 2017-03-01 12:00:00
        } else { // update token
            $uid = $this->getValidUid();
            // un not update
            // ln not update
            $this->otok = $this->getValidTok();
        }

        $tokInfo = ['exp' => $this->exp, 'uid' => $uid];

        $jsonText = json_encode($tokInfo);
        $cipherText = openssl_encrypt($jsonText, self::$cipherName, self::$cipherKey, true, self::$iv);
        $this->tok = Base64UrlSafeEncoder::encode($cipherText);
        $this->needSync = true;

        return $this->tok;
    }


    /**
     * 同步到 redis
     * @throws \Exception
     */
    public function syncTokToRedis()
    {
        if (!$this->needSync) {
            return;
        }

        if (empty($this->un) || empty($this->ln)) {
            return;
        }

        // 1 判断时间 这里不判断时间了
        // 2 判断tok是否空

        $redis = WgxRedis::getRedisObj(RedisConfig::RedisDb);
        $tKey = $this->buildTKey($this->un, $this->ln);
        if (empty($this->tok)) {
            $redis->del($tKey);
        } else {
            if ($this->otok) {
                $redis->setex($tKey, $this->exp - DateTimeHelper::timeNow(), $this->tok . '.' . $this->otok);
            } else {
                $redis->setex($tKey, $this->exp - DateTimeHelper::timeNow(), $this->tok);
            }
        }
    }

    /**
     * 同步到 browser
     */
    public function syncTokToBrowser()
    {
        if (!$this->needSync) {
            return;
        }

        // 1 判断时间 这里不判断时间了
        // 2 判断tok是否空

        if (empty($this->tok)) {
            CookieHelper::setCookie('jwt', '', DateTimeHelper::timeNow() - 10, '/');
        } else {
            $head = [
                'exp' => $this->exp,
                'un' => $this->un,
                'ln' => $this->ln,
                'otok' => '',
            ];
            if ($this->otok) {
                $head['otok'] = $this->otok;
            }

            $jsonText = json_encode($head);
            $jwtHead = Base64UrlSafeEncoder::encode($jsonText);
            $sign = md5($jwtHead . '.' . $this->tok . self::$signKey);
            $jwt = $jwtHead . '.' . $this->tok . '.' . $sign;

            CookieHelper::setCookie('jwt', $jwt, $this->exp, '/');
        }
    }

    // 测试使用
    //public function echoSyncTokToRedis()
    //{
    //    if (!$this->needSync) {
    //        echo 'syncTokToRedis none';
    //
    //        return;
    //    }
    //
    //    // 1 判断时间 这里不判断时间了
    //    // 2 判断tok是否空
    //
    //    if (empty($this->tok)) {
    //        echo 'syncTokToRedis del token : ' . $this->tok;
    //    } else {
    //        $diff = $this->exp - DateTimeHelper::timeNow();
    //        if ($this->otok) {
    //            echo 'syncTokToRedis set token : ' . $this->tok . '.' . $this->otok . ' ex : ' . $diff . PHP_EOL;
    //        } else {
    //            echo 'syncTokToRedis set token : ' . $this->tok . ' ex : ' . $diff . PHP_EOL;
    //        }
    //    }
    //}

}